﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using BeanboxSoftware.BeanMap;
using System.Threading;
using System.Text.RegularExpressions;

namespace TurtleLib.Simulation
{
  public class WorldUx
  {
    private Form _form;
    protected World _world;
    protected Map<Type, Func<object, IPanel>> _subscriptionHandlers;
    protected ManualResetEvent _readyEvent;
    protected DragComponent _dragger;
    protected Panel _canvas;

    public WorldUx(World parentWorld)
    {
      _world = parentWorld;
      _readyEvent = new ManualResetEvent(false);
      _dragger = new DragComponent();

      // register subscription handlers
      _subscriptionHandlers = new Map<Type, Func<object, IPanel>>();
      _subscriptionHandlers.DefaultValue = delegate { return null; };
      AddSubscriptionHandler<ComputerCraft.Computer>(dp => new Simulation.ComputerUx(dp));
      AddSubscriptionHandler<ComputerCraft.Turtle>(dp => new Simulation.TurtleUx(dp));
      AddSubscriptionHandler<ComputerCraft.AdvancedComputer>(dp => new Simulation.AdvancedComputerUx(dp));
      AddSubscriptionHandler<ComputerCraft.Monitor>(dp => new Simulation.MonitorUx(dp));
      AddSubscriptionHandler<ComputerCraft.AdvancedMonitor>(dp => new Simulation.AdvancedMonitorUx(dp));

      _form = new Form();
      _form.Size = new Size(1300, 800);
      _form.BackColor = Color.Black;
      _form.Text = "ComputerCraft Testing World";

      _canvas = new Panel();
      _canvas.BackColor = Color.DarkGray;
      _canvas.Location = Point.Empty;
      _canvas.Size = _form.Size;
      _canvas.MouseDown += OnMouseDown;
      _canvas.MouseWheel += OnMouseWheel;
      _canvas.Show();

      _form.Resize += delegate { StretchCanvas(); };

      _form.Controls.Add(_canvas);

      _dragger.SubscribeTo(_canvas);
      _dragger.Drag += OnDrag;

      Application.EnableVisualStyles();
    }

    protected void OnDrag(Size delta)
    {
      var nextLoc = new Point(_canvas.Location.X + delta.Width, _canvas.Location.Y + delta.Height);
      nextLoc.X = nextLoc.X > 0 ? 0 : nextLoc.X;
      nextLoc.Y = nextLoc.Y > 0 ? 0 : nextLoc.Y;

      if (nextLoc == _canvas.Location)
        return;

      _canvas.Location = nextLoc;
      StretchCanvas();
    }

    private void StretchCanvas()
    {
      Rectangle screenRectangle = _form.RectangleToScreen(_form.ClientRectangle);
      int windowBarWidth = screenRectangle.Left - _form.Left;
      int clientWidth = _form.Width - windowBarWidth * 2;
      ///
      int titleHeight = screenRectangle.Top - _form.Top;
      int clientHeight = _form.Height - titleHeight - windowBarWidth;
      ///

      int realRight = (-_canvas.Location.X) + clientWidth;
      int realBottom = (-_canvas.Location.Y) + clientHeight;

      _canvas.Size = new Size(realRight, realBottom);
    }

    public Color BackColor { get { return _form.BackColor; } }
    public World World { get { return _world; } }

    public void OnMouseWheel(object sender, MouseEventArgs e)
    {
      var activePanel = ActivePanel;
      if (activePanel == null)
        return;

      activePanel.Expand(e.Delta < 0 ? false : true, 10);
    }

    protected void OnMouseDown(object sender, MouseEventArgs e)
    {
      if (e.Button == MouseButtons.Left)
      {
        if (ActivePanel == null)
        {
          Console.WriteLine("no active panel to deselect");
          return;
        }

        OnDragablePanelActivated(null);
      }
      else if (e.Button == MouseButtons.Middle)
      {
        OnDrag(new Size(-_canvas.Location.X, -_canvas.Location.Y));
        OnDragablePanelActivated(null);
        SortPanels();
      }
    }

    public void SortPanels()
    {
      var sortableOrder = Panels.OrderByDescending(p => Math.Sqrt(Math.Pow(p.Width, 2) + Math.Pow(p.Height, 2))).ToList();

      // first move all panels off screen
      Panels.ToList().ForEach(child =>
      {
        child.Location = new Point(-child.Width, -child.Height);
      });

      int workingHeight = Math.Min(_canvas.Height, 400);

      sortableOrder.ToList().ForEach(child =>
      {
        // find first free x on y=0
        int y = 0;
        int x = 0;

        // search down along x
        int minWidth = int.MaxValue;
        Point next = new Point(x, y);
        while (true)
        {
          next = new Point(x, y);
          var panel = PanelAtPoint(next);

          if (panel == null)
          {
            if ((y < workingHeight) && IsPointValidPlacement(next, child, allowXPos: true))
              break;

            if (minWidth == int.MaxValue)
            {
              if (y > workingHeight)
              {
                y = 0;
                x += child.Width;
              }
              else
                y += child.Height;
            }
            else
            {
              y = 0;
              x += minWidth;
              minWidth = int.MaxValue;
            }

            continue;
          }

          minWidth = Math.Min(minWidth, panel.Width - (x - panel.Left));
          y = panel.Bottom;
        }

        child.Location = next;
      });
    }

    public DragablePanel ActivePanel
    {
      get
      {
        return Panels.FirstOrDefault(p => p.IsActive);
      }
    }

    public IEnumerable<DragablePanel> Panels
    {
      get
      {
        return _canvas.Controls
          .Cast<Control>()
          .Where(c => c is DragablePanel.CustomPanel)
          .Select(c => (c as DragablePanel.CustomPanel).Target);
      }
    }

    private void AddSubscriptionHandler<CC>(Func<DragablePanel, ISubscriber<CC>> getSubscriber)
    {
      Func<object, IPanel> subscribe = obj =>
      {
        var dragPanel = new DragablePanel(this);
        var subscriber = getSubscriber(dragPanel);
        var host = (CC)obj;

        Action buildUx = () =>
        {
          AddChildPanel(dragPanel);
          subscriber.SubscribeTo(host);
        };

        if (_form.InvokeRequired)
        {
          _form.Invoke(buildUx);
        }
        else
          buildUx();

        return subscriber.Parent;
      };

      _subscriptionHandlers[typeof(CC)] = subscribe;
    }

    public void AddChildPanel(IPanel child)
    {
      child.Activated += OnDragablePanelActivated;
      child.Show(true);

      _canvas.Controls.Add(child.Handle);
    }

    public Size GetSizeEdge(DragablePanel panel, Size goal)
    {
      Size best = panel.Size;

      while (true)
      {
        int dx = best.Width < goal.Width ? 1 : best.Width > goal.Width ? -1 : 0;
        int dy = best.Height < goal.Height ? 1 : best.Height > goal.Height ? -1 : 0;

        if (dx == 0 && dy == 0)
          break;

        var next = new Size(best.Width + dx, best.Height + dy);
        bool collision = PanelsInArea(new Rectangle(panel.Location, next), panel).FirstOrDefault() != null;

        if (collision)
          break;

        best = next;
      }

      return best;
    }

    public Point GetPointEdge(DragablePanel panel, Point dst)
    {
      Point approach = panel.Location;

      // if target is good, allow it without adjustment
      if (IsPointValidPlacement(dst, panel))
      {
        return dst;
      }

      Point best = approach;

      int dx = (dst.X - best.X) < 0 ? -1 : 1;
      for (int x = best.X; ; x += dx)
      {
        var next = new Point(x, best.Y);
        if (IsPointValidPlacement(next, panel))
          best = next;

        if (x == dst.X)
          break;
      }

      int dy = (dst.Y - best.Y) < 0 ? -1 : 1;
      for (int y = best.Y; ; y += dy)
      {
        var next = new Point(best.X, y);
        if (IsPointValidPlacement(next, panel))
          best = next;

        if (y == dst.Y)
          break;
      }

      Point punch = new Point(Normalize(dst.X - best.X), Normalize(dst.Y - best.Y));
      var ghost = panel.Bounds;
      ghost.Location = best;

      var nudgeContacts = GetCollisions(panel, ghost, punch);
      var moved = Nudge(nudgeContacts);

      var final = new Point(best.X + moved.X, best.Y + moved.Y);

      return final;
    }

    private class CollisionToken
    {
      public CollisionToken(Point originalPunch)
      {
        XBlocked = originalPunch.X == 0;
        YBlocked = originalPunch.Y == 0;
      }

      public bool XBlocked { get; private set; }
      public bool YBlocked { get; private set; }
      public bool Blocked { get { return XBlocked && YBlocked; } }

      public bool Blocks(Point push)
      {
        if (push.X != 0 && XBlocked)
          return true;

        if (push.Y != 0 && YBlocked)
          return true;

        return false;
      }

      public Point Clean(Point push)
      {
        Point cleaned = new Point();
        cleaned.X = XBlocked ? 0 : push.X;
        cleaned.Y = YBlocked ? 0 : push.Y;

        return cleaned;
      }

      public bool ValidateBoundary(Rectangle parent, Rectangle child, Point punch, bool ignorePos = false)
      {
        var boundaryCheckResult = RectangleBoundaryCheck(parent, BoxAtOffset(child, punch));

        // ignorePos allows off-screen nodes to push nodes around that are in-screen
        boundaryCheckResult &= ~(ignorePos ? BoundaryCheckResult.POS : BoundaryCheckResult.Good);

        // a block can only block in the direction it is being punched
        boundaryCheckResult &= ~(punch.X == 0 ? (BoundaryCheckResult.X_VALUE | BoundaryCheckResult.X_OUT) : 0);
        boundaryCheckResult &= ~(punch.Y == 0 ? (BoundaryCheckResult.Y_VALUE | BoundaryCheckResult.Y_OUT) : 0);

        if ((boundaryCheckResult & BoundaryCheckResult.X_VALUE) != 0)
          XBlocked = true;
        if ((boundaryCheckResult & BoundaryCheckResult.Y_VALUE) != 0)
          YBlocked = true;

        return boundaryCheckResult == BoundaryCheckResult.Good;
      }
    }

    private IEnumerable<Tuple<DragablePanel, Point>> GetCollisions(DragablePanel panel, Rectangle ghost, Point punch, CollisionToken token = null)
    {
      bool isTop = false;
      if (token == null)
      {
        token = new CollisionToken(punch);
        isTop = true;
      }

      if (punch == Point.Empty)
        yield break;

      var collidingBox = BoxAtOffset(ghost, punch);
      var collidedPanels = PanelsInArea(collidingBox, panel).ToList();

      foreach (var hitPanel in collidedPanels)
      {
        int px = 0;
        int py = 0;

        // push x
        int hitLeft = hitPanel.Location.X;
        int hitRight = hitLeft + hitPanel.Width;
        int panLeft = ghost.Location.X;
        int panRight = panLeft + ghost.Width;
        int rightSeparation = hitLeft - panRight;
        int leftSeparation = panLeft - hitRight;
        int xseparation = Math.Max(rightSeparation, leftSeparation);
        if (!token.XBlocked && xseparation >= 0)
        {
          px = Normalize(punch.X);
        }

        // push y
        int hitTop = hitPanel.Location.Y;
        int hitBot = hitTop + hitPanel.Height;
        int panTop = ghost.Location.Y;
        int panBot = panTop + ghost.Height;
        int botSeparation = hitTop - panBot;
        int topSeparation = panTop - hitBot;
        int yseparation = Math.Max(botSeparation, topSeparation);
        if (!token.YBlocked && yseparation >= 0)
        {
          py = Normalize(punch.Y);
        }

        if (px == 0 && py == 0)
        {
          continue;
        }

        var isolatedPunch = new Point(px, py);

        // if hitPanel would move out of bounds, it is hard locked
        if (!token.ValidateBoundary(GetGraphicsPaneBox(), hitPanel.Bounds, isolatedPunch))
        {
          // who ever was pushing me -- cannot push me
          if (token.Blocked)
            break;

          continue;
        }

        foreach (var child in GetCollisions(hitPanel, hitPanel.Bounds, isolatedPunch, token))
        {
          if (!token.Blocks(child.Item2))
            yield return child;
        }
      }

      token.ValidateBoundary(GetGraphicsPaneBox(), ghost, punch, isTop);
      var final = token.Clean(punch);

      yield return Tuple.Create(panel, final);
    }

    private Point Nudge(IEnumerable<Tuple<DragablePanel, Point>> nudgeContacts)
    {
      var nudgeList = nudgeContacts.ToList();
      int hitCount = nudgeList.Count;
      if (hitCount < 2) // the hitting panel alone means it was blocked
        return Point.Empty;

      // blocks can only move as much as the target node moves
      var targetMovement = nudgeList.Last().Item2;

      foreach (var contact in nudgeList)
      {
        var panel = contact.Item1;
        var dir = contact.Item2;

        // correct movement if target is not moving
        dir.X = targetMovement.X == 0 ? 0 : dir.X;
        dir.Y = targetMovement.Y == 0 ? 0 : dir.Y;

        var loc = panel.Location;
        loc.Offset(dir);
        panel.Location = loc;
      }

      return nudgeList.Last().Item2;
    }

    public bool IsPointValidPlacement(Point point, DragablePanel panel, bool allowXPos = false)
    {
      var pointResult = PointBoundaryCheck(point);
      if (pointResult != BoundaryCheckResult.Good)
      {
        if (!allowXPos)
          return false;

        if (allowXPos && (pointResult != BoundaryCheckResult.X_POS))
          return false;
      }

      var area = panel.Bounds;
      area.Location = point;
      return PanelsInArea(area, panel).FirstOrDefault() == null;
    }

    private IEnumerable<DragablePanel> PanelsInArea(Rectangle area, DragablePanel ignore = null)
    {
      return Panels.Where(dp => area.IntersectsWith(dp.Bounds)).Where(p => p != ignore);
    }

    public BoundaryCheckResult RectangleBoundaryCheck(Rectangle area)
    {
      return RectangleBoundaryCheck(GetGraphicsPaneBox(), area);
    }

    public static BoundaryCheckResult RectangleBoundaryCheck(Rectangle parent, Rectangle area)
    {
      BoundaryCheckResult result = BoundaryCheckResult.Good;

      if (area.Location.X < 0)
        result |= BoundaryCheckResult.X_NEG;

      if (area.Location.X > (parent.Width - area.Width))
        result |= BoundaryCheckResult.X_POS;

      if (area.Location.Y < 0)
        result |= BoundaryCheckResult.Y_NEG;

      if (area.Location.Y > (parent.Height - area.Height))
        result |= BoundaryCheckResult.Y_POS;

      return result;
    }

    public BoundaryCheckResult PointBoundaryCheck(Point point)
    {
      var box = GetGraphicsPaneBox();
      var child = new Rectangle(point, new Size(0, 0));
      return RectangleBoundaryCheck(box, child);
    }

    public static Rectangle BoxAtOrigin(Rectangle box)
    {
      return new Rectangle(new Point(0, 0), box.Size);
    }

    public static Rectangle BoxAtOffset(Rectangle box, Point offset)
    {
      Rectangle copy = box;
      copy.Offset(offset);
      return copy;
    }

    public DragablePanel PanelAtPoint(Point point, DragablePanel ignore = null)
    {
      var hit = Panels.Where(p => p != ignore).FirstOrDefault(d => d.Bounds.Contains(point));
      return hit as DragablePanel;
    }

    public bool IsPointOnPanel(Point point, DragablePanel ignore = null)
    {
      var hit = PanelAtPoint(point, ignore);
      return hit != null;
    }

    protected void OnDragablePanelActivated(DragablePanel panel)
    {
      Panels.Where(p => p != panel).ToList().ForEach(p => p.Select(false));
    }

    public void Run()
    {
      UpdatePanels();
      SortPanels();

      _form.Shown += delegate { _readyEvent.Set(); };

      Application.Run(_form);
    }

    private void UpdatePanels()
    {
      Panels.ToList().ForEach(p =>
      {
        var oldPoint = p.Location;
        p.Location = new Point(-10000, -10000);
        p.Update();
        p.Location = oldPoint;
      });
    }

    public Func<object, IPanel> GetSubscriberHandler(Type type)
    {
      return _subscriptionHandlers[type];
    }

    public void Remove(IPanel panel)
    {
      panel.Show(false);
      _canvas.Controls.Remove(panel.Handle);
    }

    public void WaitUntilReady()
    {
      _readyEvent.WaitOne();
    }

    private static int Normalize(int i)
    {
      return i > 0 ? 1 : i < 0 ? -1 : 0;
    }

    public static Color ConvertColor(ComputerCraft.colors colorId)
    {
      Color c = new Color();
      switch (colorId)
      {
        case ComputerCraft.colors.white: c = Color.White; break;
        case ComputerCraft.colors.orange: c = Color.Orange; break;
        case ComputerCraft.colors.magenta: c = Color.Magenta; break;
        case ComputerCraft.colors.lightBlue: c = Color.LightBlue; break;
        case ComputerCraft.colors.yellow: c = Color.Yellow; break;
        case ComputerCraft.colors.lime: c = Color.Lime; break;
        case ComputerCraft.colors.pink: c = Color.Pink; break;
        case ComputerCraft.colors.gray: c = Color.Gray; break;
        case ComputerCraft.colors.lightGray: c = Color.LightGray; break;
        case ComputerCraft.colors.cyan: c = Color.Cyan; break;
        case ComputerCraft.colors.purple: c = Color.Purple; break;
        case ComputerCraft.colors.blue: c = Color.Blue; break;
        case ComputerCraft.colors.brown: c = Color.Brown; break;
        case ComputerCraft.colors.green: c = Color.Green; break;
        case ComputerCraft.colors.red: c = Color.Red; break;
        case ComputerCraft.colors.black: c = Color.Black; break;
      }

      return c;
    }

    private Rectangle GetGraphicsPaneBox()
    {
      return new Rectangle(Point.Empty, _canvas.Size);
    }
  }
}
